Spring은 JVM 위에서 동작하는 웹 애플리케이션 프레임워크 생태계이다. 처음으로 공개된 건 2003년 Apache 2.0 License로 Spring 1.0 버전이 공개되었으며 당시 J2EE/EJB의 복잡성과 생산성 문제를 비판, 대안하기 위해 제시되었다. 이후 Spring에 여러 릴리즈와 기술이 추가되면서 빠르게 성장했으며 특히 한국에서는 다수의 기업들이 Spring 프레임워크를 사용하며 가장 많이 사용되는 웹 프레임워크 중 하나로 자리매김했다.
2025 StackOverFlow Web Frameworks Survey
Spring은 자체적으로 여러 보안 관련 기술을 제공한다. 특히 Spring Security가 기반이 되며 인증/인가 제어, XSS 방어, CSRF 보호화 등 여러 기업이 사용하기 편하도록 다양한 보안 정책을 활용하고 있다. 이 때문에 해커들의 입장에서는 Spring 기반 서비스를 공격하기가 까다로운 편이다. 다른 프레임워크들에 비해 공격 벡터가 적을 뿐더러 취약점도 찾기 힘들기 때문이다. Rewrite는 해당 연구를 통해 Spring의 작동 방식에 대해 살펴보고 최근 CVE를 조사 및 분석하여 기존의 공격 시나리오들을 파악하고 새로운 Attack Vector까지 파악해보고자 한다.
Spring vs Spring boot vs Spring Security
Spring
배경
Spring은 Java/Kotlin 애플리케이션을 위한 경량 IoC컨테이너이자 종합 애플리케이션 프레임워크로 POJO(Plain Old Java Objects)를 기반으로 애플리케이션을 구축하며 복잡한 코드를 제거하여 코드의 복잡성을 낮출 수 있는 오픈소스 프레임워크이다. 의존성 주입과 AOP를 바탕으로 객체 간의 결합도를 낮추고 유지보수성과 테스트 용이성을 확보하는 것을 목적으로 한다. 아래 챕터에선 Spring의 구성요소를 설명한다.
구성요소 및 특징
Spring 프레임워크는 약 20개의 모듈로 구성된 기능들로 이루어져 있다. 각 모듈을 Tree 형태로 구성하면 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Spring Framework ├── Core Container │ ├── Beans │ ├── Core │ ├── Context │ └── Expression Language (SpEL) ├── Data Access / Integration │ ├── JDBC │ ├── ORM │ ├── JMS │ └── Transactions ├── Web │ ├── Web (Servlet 기반) │ ├── Web MVC │ └── WebSocket, WebFlux 등 ├── AOP (Aspect Oriented Programming) └── Testing
주요 모듈 계층 구조의 도식화
Core Container(코어 스프링)
코어 컨테이너는 Core, Beans, Content, Expression Language 모듈로 구성된다.
Core, Beans
IoC 및 DI 등 프레임워크의 핵심적인 기능을 제공
IOC : Inversion Of Control, 생명주기 제어 주체의 역전
DI : Dependency Injection, 의존성 주입. 다른 의존성(객체, 클래스 등)을 주입받는 것(Class-In-Class)
Context
Core와 Beans 모듈이 제공하는 견고한 기반 위에서, 프레임워크의 일관된 API를 통해 객체를 손쉽게 조회할 수 있는 수단을 제공한다. 이는 JNDI 레지스트리 방식과 유사하다.
1
DataSource ds = (DataSource) ctx.lookup("java:/comp/env/jdbc/MyDB"); // 기존 JNDI
위와 같이 IoC를 적용하면 생성자를 통해 의존성만 선언할 수 있다. 즉, IoC는 개발자가 아닌 스프링이 특정 클래스가 사용할 객체를 생성하여 의존 관계를 맺어주는 것이다.
Spring IoC 컨테이너
DI(Dependency Injection)
DI는 위에서처럼 객체의 의존 객체를 직접 생성하지 않고 외부에서 주입하여 전달하는 설계 기법
IoC원칙을 구현하기 위해 DI라는 메커니즘을 사용
AOP (Aspect Oriented Programming)
핵심 로직과 공통 로직을 분리해두고, 런타임에 프록시나 바이트코드 조작을 통해 원하는 지점에 자동 삽입
이를 위해 공통 관심 사항과 관련된 기능들을 별도의 객체로 분리
개발자는 핵심 로직만 깔끔하게 작성하고, 공통 로직은 분리된 Aspect에서 일괄 관리
PSA (Portable Service Abstraction)
기존 데이터베이스를 다른 데이터베이스로 변경 할 시 Spring에서는 동일한 사용방법을 유지한 채로 데이터베이스를 바꿀 수 있음
이는 스프링이 서비스를 추상화한 인터페이스를 제공했기 때문인데 이를 JDBC라고 칭함
각 데이터베이스를 만드는 회사는 JDBC를 기반으로 코드를 작성하는데 이처럼 서비스를 추상화하여 일관된 방식으로 사용할 수 있게 한 것을 PSA라고 칭함
Spring Boot
배경
Spring Boot는 자동 설정과 내장 톰캣(Tomcat) 등의 런처를 통해 애플리케이션을 즉시 실행할 수 있는 환경을 제공한다. 반면 기존 Spring Framework는 애플리케이션 컨텍스트, 서블릿 설정, 의존성 관리 등을 개발자가 직접 구성해야 했으므로 초기 개발 진입 장벽이 높았다. Spring Boot는 이러한 진입 장벽을 없애고 사람들이 Spring 환경 세팅을 좀 더 편하게 할 수 있도록 여러 개발 편의성을 제공한다. 사실상의 Spring boot는 Spring으로 애플리케이션을 만들 때에 필요한 설정을 간편하게 처리해주는 별도의 프레임워크라고 볼 수 있다.
구성요소 및 특징
자동 설정 : 개발자가 명시적으로 설정하지 않아도 대부분 자동 구성
스타터 종속성 : 특정 목적의 기능들을 묶어둔 의존성 패키지
내장 톰캣 : 톰캣, Jetty등을 내장하여 WAR 배포 없이 독립 실행 가능
Production-ready Actuator : Health check, 로그조회 등의 운영 도구 제공
application.yml, application.properties등을 통한 빠른 설정 가능
Spring Security
배경
Spring Security는 Spring 애플리케이션에 인증(Authentication)과 인가(Authorization) 기능을 부여하기 위한 보안 프레임워크이다. 인증/인가 개발 패턴을 기능으로써 지원하며, 세션 기반 로그인, OAuth2, JWT 같은 다양한 인증 방식을 손쉽게 적용할 수 있다. 또한 URL 접근 제어, 메서드 단위 권한 검사, CSRF 방어, 보안 헤더 설정 등 웹 애플리케이션 보안에 필요한 핵심 기능을 제공한다. 이러한 특징 덕에 Spring 애플리케이션의 보안성을 높이기 위해 거의 필수적으로 도입되곤 한다. 그러나 보안을 위해 도입한 프레임워크에서 역으로 취약점이 발생하기 도하는데, 이는 CVE 챕터에서 다룰 예정이다.
구성요소 및 특징
Authentication / Authorization : 인증 및 인가
Filter : Middleware 와 유사함 | 서블릿 필터로 요청/응답 흐름에 개입 | 서블릿 컨테이너 레벨에서 동작FilterChain 기반으로 순차 실행
SecurityContextHolder : 현재 사용자의 보안 컨텍스트 저장
PasswordEncoder : Bcrypt 등 패스워드 암호화 수행
UserDetails / UserDetailsService : 사용자 정보 조회를 위한 인터페이스
CSRF, CORS, Session : 기본적인 웹 보안 설정 지원
또한 Spring Security는 아래 4가지 핵심 개념을 기반으로 구축되었다.
인증
권한 부여
비밀번호 저장
서블릿 필터
Spring Security를 사용하기 위해선 pom.xml에 아래와 같은 종속성을 추가하면 된다. (Maven 기준)
Spring은 여러 빌드 도구를 사용하여 간편하게 빌드할 수 있다. 특히 애플리캐이션을 개발하면서 다양한 외부 라이브러리를 다운받을 상황이 많은데 빌드 도구를 사용하면 각 라이브러리 종류와 버전을 명시만 해줌으로써 자동으로 다운로드 받아 간편히 관리할 수 있다(like Python pip). 이때 가장 많이 사용되는 것이 Maven과 Gradle이며 해당 챕터에서 두 도구에 대해 설명을 하고 각각의 Build방법에 대해 소개하겠다.
Maven
Maven은 Apache에서 만든 전통적인 빌드/프로젝트 관리 도구로 선언형 xml(pom.xml) 기반이다.
생태계가 오래되어 플러그인이나 레퍼런스가 많다는 장점이 있으며 XML기반으로 설정이 장황할 수 있지만 대규모 기업 프로젝트에서 여전히 많이 사용되곤 한다. 아래는 pom.xml 파일의 예시이다.
<dependencies> <!-- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <build> <plugins> <!-- Spring Boot Maven Plugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
Maven은 아래와 같은 명령어로 빌드 및 실행이 가능하다.
1 2 3
mvn clean package -DskipTests # 테스트 실행 생략 후 빌드 mvn clean install # 깨끗하게 빌드 후 로컬 저장소에 설치 mvn spring-boot:run # Maven플러그인으로 바로 실행
Gradle
Gradle은 Groovy / Kotlin DSL(Domain-Specific-Language)기반의 빌드 도구로 DSL문법을 사용함으로써 xml을 사용하는 Maven보다 훨씬 간결하고 성능 최적화와 유연성이 뛰어나다는 장점이 있다. 최근 Spring Boot의 기본 디폴트로 많이 사용되며 캐시나 Incremential build를 사용하여 빌드 속도가 빠르다는 장점이 있다.
2025-08-20T09:48:00.883+09:00 INFO 48585 --- [demo] [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.15 with PID 48585 (/Users/dowonjun/Desktop/VSC/study/research/spring_demo/target/classes started by dowonjun in /Users/dowonjun/Desktop/VSC/study/research/spring_demo) 2025-08-20T09:48:00.884+09:00 INFO 48585 --- [demo] [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default" 2025-08-20T09:48:01.109+09:00 INFO 48585 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 9898 (http) 2025-08-20T09:48:01.116+09:00 INFO 48585 --- [demo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2025-08-20T09:48:01.117+09:00 INFO 48585 --- [demo] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.43] 2025-08-20T09:48:01.134+09:00 INFO 48585 --- [demo] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2025-08-20T09:48:01.134+09:00 INFO 48585 --- [demo] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 230 ms 2025-08-20T09:48:01.223+09:00 INFO 48585 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 9898 (http) with context path '/' 2025-08-20T09:48:01.227+09:00 INFO 48585 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.46 seconds (process running for 0.55) 2025-08-20T09:48:04.207+09:00 INFO 48585 --- [demo] [nio-9898-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2025-08-20T09:48:04.207+09:00 INFO 48585 --- [demo] [nio-9898-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2025-08-20T09:48:04.208+09:00 INFO 48585 --- [demo] [nio-9898-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
./mvnw
프로젝트에 지정된 Maven 버전을 자동 다운로드 및 실행함 → 팀원 간 Maven 버전 차를 방지
spring-boot-maven-plugin 의 run 을 실행
run 외에 repackage , stop 등 있으며(각각 다른 동작 로직을 내포) run 은 소스코드 컴파일 후 실행을 담당
Build Process
소스코드 컴파일 & 리소스 복사
compile phase를 수행 → target/classes 생성
ClassPath 결합
target/classes + dependencies (Maven이 가져온 jar) → 실행용 classpath 구성
메인 클래스 탐색
@SpringBootApplication 이 붙은 클래스(DemoApplication)을 엔트리포인트로 지정
또는 pom.xml에 start-class 지정 가능
JVM 프로세스 실행
org.springframework.boot.devtools.restart.RestartLauncher 같은 부트 런처가 main() 실행
DevTools : 코드 변경 감지 시 자동 리스타트 지원
SpringApplication.run() 호출
IoC 컨테이너 생성, Bean 스캔, 의존성 주입(DI), 내장 톰캣/Jetty/Undertow 구동 … 등
Spring 최적화
Spring은 위에서 설명한 Gradle, Maven과 같은 도구들을 활용하여 빌드 최적화를 개선시키려 노력 하고 있다. 예시로 Spring Boot3부터는 GraalVM Native Image를 지원한다. 일반적인 Spring 애플리케이션은 JAR / WAR형태로 패키징하고 실행 시 JVM 위에서 동작하지만 Native Image 방식은 GraalVM이 제공하는 AOT(Ahead Of Time) 컴파일러로 미리 기계어 바이너리를 만들어 놓는다. 즉 JVM을 사용할 필요 없이 바로 실행 가능한 실행파일을 얻을 수 있다. 이를 통해 수십 밀리초(JVM보다 약 50배 빠름) 내에 시작하는 작은 컨테이너의 형태로 Spring 애플리케이션을 배포할 수 있다.
GraalVM vs JVM
빌드 시간
기존 JVM이 빌드되기까지는 몇 초면 가능
GraalVM Native Image 빌드는 미리 코드를 전부 기계어로 컴파일 하기 때문에 수 분이 걸림
메타데이터
JVM은 런타임에 리플렉션, 프록시, 동적 클래스 로딩 등을 자연스럽게 처리
GraalVM의 네이티브 이미지는 정적 컴파일이라 동적 기능을 사용하기 위해선 metadata가 필요
Spring이 많은 부분을 자동 추론해주지만, 외부 라이브러리는 직접 metadata를 지정해야 정상 작동함
Classpath 및 Bean 조건 고정
GraalVM의 네이티브 빌드 시점에 Classpath와 Bean조건이 고정
런타임 중에 DB의 URL/비밀번호 등은 변경 가능하지만 DB유형을 변경하거나 Spring Bean의 구조를 변경하는 작업은 불가
Spring 구성 요소에 대한 이해
해당 챕터는 Spring에서 사용되는 함수, 구성요소의 코드레벨 동작과 동작을 분석하기 위한 디버깅 방식에 대한 내용을 포함한다. Spring의 코드레벨 단 동작을 분석하기 위해 빌드 과정 및 디버깅 방법에 대해 조사하였으며 그에 대한 내용을 서술하였다.
How to Debug Spring Application
Visual Studio Code를 기반으로 애플리케이션 디버깅 방식을 소개한다.
VSCODE 확장팩 설치
Extension Pack for java
Spring Boot Extension Pack
구축한 어플리케이션의 start-class 파일로 가서 command + shift + D
이와 같은 방식을 통해 쉽게 Spring 애플리케이션을 디버깅할 수 있으며 이외에도 JDB를 통해 CLI 환경에서 디버깅 환경을 구축하는 것도 가능하다. Rewrite의 리서처들은 위 방식을 통해 Spring 앱을 디버깅하고 각종 구성요소의 동작을 파악하였다.
Spring Annotation
Spring 어노테이션은 애플리케이션 개발 과정에서 반복적으로 작성하던 설정과 코드를 단순화하기 위해 제공되는 메타데이터 표기 방식이다. 클래스, 메서드, 필드 등에 붙여 특정 동작이나 설정을 선언적으로 지정할 수 있으며 대표적으로 @Component, @Service, @Repository, @Transactional 등이 존재한다. 이처럼 Spring 어노테이션은 XML 기반 설정을 대체하고, 코드 가독성과 유지보수성을 높이며, 개발자가 비즈니스 로직에 집중할 수 있도록 지원한다. 아래는 이러한 어노테이션의 종류이다.
스테레오타입 (빈 등록 관련)
@Component → 일반 Bean 등록
@Service → 서비스 계층 Bean 등록
@Repository → DAO/Repository 계층 Bean 등록 (예외 변환 기능 포함)
@Controller → MVC 컨트롤러 등록
@RestController → REST API 컨트롤러 (@Controller + @ResponseBody)
Spring security에서 사용되는 Security Annotation의 잘못된 사용으로 인해 이를 우회할 수 있는 취약점이다.
@EnableMethodSecurity 를 사용하는 환경에서 보안 어노테이션(@PreAuthorize, @Secured 등)이 제네릭 기반 (superclass, interface) 선언부 또는 오버라이드 메서드에만 붙어있고 실제 타겟 메서드에서는 어노테이션이 없는 경우에 인증 우회가 가능하다.
<!-- Spring Security Web --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring.security.version}</version> </dependency>
이때, 스캐너가 자식 클래스의 실제 오버라이드 메소드를 찾기 위해 targetClass.getDeclaredMethod(method.getName(), method.getParameterTypes()) 같은 소거 기반 시그니처 매칭을 사용한다. 이러한 소거 기반 시그니처 매칭을 사용하면 AbstractImpl 의 실제 구현된 mutate 경우엔 AccountSecret mutate(AccountSecret) 으로 보지만 오버라이드 된 AbstractSecureApi 의 브리지 메서드 mutate는 Object mutate(Object) 와 같은 형태로 보기 때문에 서로 어노테이션이 없는것으로 보게 된다. 따라서 상위 선언부에 붙은 어노테이션을 놓치게 된다.
Part2. PoC
위 취약점 때문에 아래 Controller에서 user의 권한으로도 /pocA 와 /pocB 에 접근이 가능하다.
가장 구체적인 실제 호출 대상을 얻기 위해 브리지 / 공변 / 프록시까지 해석한 다음, 어노테이션은 다음 우선순위로 병합 탐색을 진행한다.
실제 호출될 구체 메서드
필요한 경우 선언 클래스/인터페이스 수준
그 메서드가 브리지인 경우 브리지 원본 메서드
파라미터화된 상위 타입/인터페이스의 대응 메서드
이렇게 하면 제네릭 치환된 상위 선언부에 붙은 보안 어노테이션도 타깃 메서드에 귀속된 것으로 평가되므로 우회가 불가능해진다.
CVE-2025-41232
CVE-2025-41232 는 특정 spring-security-core 버전에서 Spring 보안 어노테이션이 적용된 메서드를 탐지하는 로직이 잘못 구성되어 보안 요소가 우회될 수 있는 취약점이다. 특이하게도 해당 취약점은 위에서 소개된 CVE-2025-22223 취약점을 패치하기 위해 제작된 코드에서 발생하였으며 취약점 발생을 위한 구성 조건이 복잡하지 않아 많은 Spring 애플리케이션이 해당 취약점의 영향을 받았다. 아래는 해당 취약점에 대한 분석 내용이다.
Affected
spring security 6.4.0 - 6.4.5
@EnableMethodSecurity(mode=ASPECTJ) 사용
spring-security-aspects 사용
private/protected Method에 보안 어노테이션 사용 ex) @PreAuthorize
Spring에서 메서드를 보호하기 위한 2가지 방식이 존재한다. 하나는 메서드 실행 전 호출 흐름을 가로채 권한을 검증하는 방식인 Based Proxy 방식이 있으며, 다른 방식은 AspectJ를 이용해 바이트코드 위빙으로 보안 로직을 직접적으로 삽입하는 Based AspectJ 방식이 존재한다.
Based Proxy
Spring AOP(프록시 기반)로 메서드 호출을 가로채서 권한 검증을 수행
Only Public Method
final, private, static 메서드는 적용 불가
일반적으로 가장 많이 사용
Based AspectJ
AspectJ를 이용해 바이트코드 위빙으로 권한 검증 로직을 삽입
자바 소스를 컴파일하면 .class 가 생김
JVM이 .class 파일을 메모리에 올릴 때 권한 검증 로직이 삽입됨
Original 소스를 바꾸지 않고도 실행 시점에 새로운 동작(로그, 권한검사, 트랜잭션 관리 등)을 추가 가능
프록시 방식보다 강력하지만, 설정이 복잡하고 aspectjweaver javaagent 필요 Weaving Types
Compile Type Weaving (CTW)
javac → .class 만들 때 코드를 삽입 Post Compile Weaving (Binary Weaving)
이미 컴파일된 .class 또는 .jar에 다시 위빙해서 새로운 .class 생성 Load-time Weaving (LTW)
JVM이 클래스를 로드하는 순간 javaagent가 개입해서 바이트코드를 수정
Spring Security에서 AspectJ 모드를 쓸 때 필요한 방식
CVE-2025-41232 는 Based AspectJ 방식에서 발견되었다. AspectJ 방식에서 호출되는 함수 내에 잘못 설계된 메서드 탐지 로직이 포함되었기 때문이다. 이를 이해하기 위해선 보안 어노테이션이 적용된 메서드를 탐색하는 과정에 대한 이해가 필요하다. 일반적으로 @PreAuthorize("hasRole('X')") 같은 메서드 보안 어노테이션은 리플렉션(reflection) 으로 읽어서 동작하도록 설계 되어있다.
리플렉션
JVM 실행 중에 클래스 / 메서드 / 필드 같은 프로그램 구조를 조사하고, 심지어 호출·수정까지 할 수 있게 해주는 기능
→ 코드를 하드코딩하지 않고, 실행 중에 동적으로 프로그램의 구조를 들여다보고 제어하는 기능
그러나 이러한 리플렉션은 호출 비용이 크고, 상속 / 인터페이스 / 브리지 메서드까지 탐색하는 경우 특히 느리다는 특징을 지니고 있다. 실제 서비스에서 컨트롤러 ↔ 서비스는 수천만번도 호출될 수 있는데 이를 수행 할때마다 리플렉션으로 어노테이션을 읽으면 퍼포먼스 하락 이슈가 발생할 수 밖에 없다. 따라서 Spring에서 보안 어노테이션을 읽고 실행하는 동작은 첫 호출에만 리플렉션 스캔을 수행하고 이후 ConcurrentHashMap 캐시에서 O(1) 로 조회하도록 구현되었다.
이때 리플렉션 스캔 이후 캐시 데이터를 생성하는 함수가 바로 merge() 함수이다. 메서드 / 파라미터 보안 어노테이션을 스캔해서 캐시에 넣고 이후부턴 캐시에 있는 걸 조회해서 반환(cache key : new MethodClassKey(method, targetClass))하는 동작을 수행한다. 여기서 merge() 함수는 보안 어노테이션이 적용된 메서드를 스캔하기 위해 findMethodAnnotations(Method method, Class<?> targetClass) 함수를 호출한다(현재 요청 처리 과정에서 필요한 어노테이션만 스캔함).
여기서 메서드를 탐색하는 이유는 어노테이션이 적용된 메서드가 인터페이스나 상위 클래스에 선언된 메서드일 수 있기 때문이다(동일한 메서드가 오버라이드 되는 환경일 수 있으므로). ClassUtils.getMostSpecificMethod(...)를 호출하여 어노테이션이 탐지된 클래스(targetClass)를 기준으로 실제 실행될 메서드를 가져오며, 이를 기반으로 findClosestMethodAnnotations() 함수를 호출한다.
런타임에 호출되는게 targetClass 구현체의 메서드기 때문에 오버라이드 된 메서드 가져옴
해당 함수는 구체 메서드를 기반으로 어노테이션을 탐색하는데, 그 이유는 어노테이션이 구현체 메서드가 아니라 인터페이스 선언부, 상위 클래스 등에도 있을 수 있기 때문이다. 우선 specificMethod 자체에서 보안 어노테이션을 탐색하며 이 과정에서 어노테이션이 발견되지 않을 경우 상위 클래스 / 인터페이스까지 재귀적으로 타고 올라가며 “가장 가까운 어노테이션”을 찾아낸다.
해당 과정이 재귀적 호출로 이루어짐
결과적으로 인터페이스 / 상위 클래스 어노테이션까지 모두 탐색되긴 하지만 가장 먼저 탐색된 “가장 가까운 어노테이션”을 반환함
코드 로직을 살펴보면 getDeclaredMethods() 함수를 호출하여 targetClass에 선언된 모든 메서드 객체를 꺼내서 candidate 에 순회하면서 할당한다. 이후 candidate 와 메서드 파라미터로 전달된 method 를 == 연산을 기반으로 “레퍼런스 동일성”을 비교하고 만약 동일하다면 동일하다고 판단된 candidate 객체를 반환하며 해당 검사 이후엔 isOverride(method, candidate 함수를 통해 오버라이드 관계성 비교를 수행하고 그 결과 여부에 따라 candidate or null 를 반환한다. 여기서 메서드 객체가 성공적으로 반환되면 해당 메서드를 기반으로 실제 리플렉션 호출 로직이 이뤄지고 결과적으로 캐시에 데이터가 추가된다.
Part2. Root Cause
환경 구성
루트커즈 분석을 위해 breakpoint를 spring-security-core 내부 코드에 걸어야 했다. 따라서 위에서 서술한 VSC 기반 GUI 디버거가 아닌 java 디버깅 포트를 열고 JDB를 attatch하여 분석하는 방식을 채택하였다.
findMethod() 함수는 파라미터로 전달 받은 method 객체와 getDeclaredMethods() 함수로 꺼내온 객체를 == 연산으로 비교한다. 즉 레퍼런스 동일성 비교를 수행하는 셈인데, 동일 시그니처라도 Method 인스턴스가 다르기 때문에 레퍼런스 동일성 검사는 실패하게 된다. Method 는 리플렉션 핸들이라 생성 경로에 따라 서로 다른 인스턴스가 되며 이는 AspectJ 위빙/프록시/다른 코드 경로로 얻은 method 객체와 targetClass.getDeclaredMethods()가 만들어낸 method (candidate)는 다르다는 것를 의미한다. 따라서 해당 검증은 실패하게 된다. 이러한 사실은 jdb를 통해 확인할 수 있다.
protectedvoiddoBind(MutablePropertyValues mpvs) { checkAllowedFields(mpvs); //바인딩이 가능한 허용 필드인지 검증 checkRequiredFields(mpvs); //필수 필드가 모두 존재하는지 확인 applyPropertyValues(mpvs); //실제 객체에 바인딩 }
mpvs 에는 쿼리 스트링 및 기타 요청 파라미터에서 추출된 값들이 저장되어 있다.
setDisallowedFields 를 설정한 경우 checkAllowedFields 에서 검증을 수행하게 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
protectedvoidcheckAllowedFields(MutablePropertyValues mpvs) { PropertyValue[] pvs = mpvs.getPropertyValues(); for (PropertyValue pv : pvs) { Stringfield= PropertyAccessorUtils.canonicalPropertyName(pv.getName()); if (!isAllowed(field)) { mpvs.removePropertyValue(pv); getBindingResult().recordSuppressedField(field); if (logger.isDebugEnabled()) { logger.debug("Field [" + field + "] has been removed from PropertyValues " + "and will not be bound, because it has not been found in the list of allowed fields"); } } } }
파라미터 값들을 배열로 가져와 순회하며 필드 이름을 정규화한 뒤 허용,금지 목록을 검증하여 특정 필드를 바인딩 대상에서 제외한다.
금지 필드와 요청 파라미터에 적용하던 사전 정규화를 제거하고 disallowedFields 검사 자체를 케이스 무시 방식으로 수정했다.
Conclusion
Spring 리서치를 통해 개념적으로 알고 있었던 보안 어노테이션, 모델 권한 검증 등 Spring 보안적 요소의 코드 레벨 단 동작을 이해하고 학습할 수 있었으며 일부에 불과하겠지만 Spring를 이루는 구성 요소와 특징, 각 계층에 대해서도 학습할 수 있었다. 또한 리서치를 진행하면서 단순히 프레임워크가 제공하는 보안 기능을 “있다/없다” 수준에서 이해하는 것이 아니라, 내부 동작 원리와 한계점을 파악하는 것이 얼마나 중요한지 체감할 수 있었다. 예를 들어, 어노테이션 기반 접근제어가 어떻게 메서드 단위로 스캔되고 병합되는지, 프록시와 AOP를 통해 호출 흐름이 어떻게 제어되는지를 코드 단에서 확인하면서 “보안 로직이 프레임워크 레벨에서 주입된다”는 말이 실제로 무엇을 의미하는지 명확히 알 수 있었다.
결과적으로 이번 리서치를 통해 단순히 Spring 보안 기능의 개념을 아는 수준을 넘어, “왜 해당 보안 요소가 필요하며, 어떻게 동작하고, 어떤 조건에서 무력화될 수 있는가”라는 본질적인 질문을 던지고 답을 찾는 과정이 중요하다는 것을 느꼈다. 향후 프레임워크 보안 요소를 학습할 때 단순 사용법에 머무르지 않고, 내부 구조와 실제 공격 시나리오를 염두에 둔 심화 학습을 지속적으로 진행할 필요성을 크게 느꼈다.